【Python】TF-IDF を使って自分のブログの特徴を取得してみた
TF-IDF の理解のために、表題の内容やってみました。
目次
- やったこと
- TF-IDF とは
- やってみた #セットアップ編
- やってみた #Beautiful Soupでテキスト取得編
- やってみた #Janome で形態素解析編
- やってみた #scikit-learn で単語の出現頻度計算編
- やってみた #TF-IDFを計算してみよう編
- おわりに
やったこと
自分のブログの TF-IDF値を調べてみました
- TF-IDFとは?の章はこちら
- 調べてみた結果の章はこちら
- TF-IDFを調べる過程はこちら (↓)
- 今回の Tryで使用した Notebookはこちら (↓)
TF-IDF とは
まず簡単に TF-IDF について説明します。
TF-IDF は 単語の重要度 を測るための指標の1つです。 TF値, IDF値の 積 を取ります。
- TF(Term Frequency): ある文書における 単語の出現頻度
- IDF(Inverse Document Frequency): 逆文書頻度。ざっくりいうと 単語のレア度
TF, IDF, TF-IDF は以下で定義されます。
x1
: 文書d における全単語の出現回数の和y1
: 文書d における単語wの出現回数x2
: 単語w を含む文書数y2
: 全文書数
TF-IDFの例
ざっくり TF-IDFの必要性をイメージしていただくために。以下例を示します。
ある AWS SageMaker 関連のブログがあったとします。 このブログで 高いTF値(出現頻度)を取る単語 は何でしょう?
例えば以下があります
- 「SageMaker」「機械学習」
- 「こと」「もの」「時」
前者はまさに ブログの特徴 として抜き出したい単語というのが直感的に分かると思います。 後者は日本語を使う上で頻出する 正直あまり重要ではない 単語ですよね。
TF-IDFは この 頻出するあまり重要ではない単語
を除外(filter)するための指標です。
これら「こと」「もの」「時」などの単語は、 もちろん他の日本語のブログでもよく見かけます。 つまり IDF値(レア度)は低い です。
まとめると以下の表になります。
単語 | TF値 | IDF値 | TF-IDF値 |
---|---|---|---|
「SageMaker」「機械学習」 | 高い | 高い | 高い |
「こと」「もの」「時」 | 高い | 低い | 低い |
TF-IDF値の高い単語がそのブログの特徴を表している ということが何となく伝わったかと思います。
やってみた #セットアップ編
Jupyter Notebook 環境を作っていきます。
Python 仮想環境の作成・有効化
以下実行します。
python -m venv venv source venv/bin/activate
VSCodeで Jupyter Notebook起動
今回は VSCode 上で Jupyter Notebookを動かしていきます。
touch try_tfidf.ipynb code .
try_tfidf.ipynb
を開きます。
必要パッケージのインストール
以下のパッケージを利用するので、インストールしていきます。
!pip install requests !pip install beautifulsoup4 !pip install janome !pip install scikit-learn !pip install pandas !pip list
※その後の処理で パッケージ import
が失敗する場合
インストールしたパッケージの場所が path
に登録されていない可能性があります。
その場合は以下のように path
を追加します。
import sys venv_packages_path = '(PROJECT_PATH)/venv/lib/python3.7/site-packages' #edit yourself if venv_packages_path not in sys.path: sys.path.append(venv_packages_path)
やってみた #Beautiful Soupでテキスト取得編
取得するブログのURLリストの準備
今回は私がこれまで書いてきた 48本のブログ を対象にします。
URLリストを ./contents/url_list.txt
に格納しています。
url_list_path = "./contents/url_list.txt" #edit yourself url_list = [] with open(url_list_path) as f: url_list = [l.replace('\n', '') for l in f.readlines()] print("len(url_list): {}".format(len(url_list))) print("url_list[0]: {}".format(url_list[0])) # len(url_list): 48 # url_list[0]: https://dev.classmethod.jp/cloud/aws/on-premise-reverse-proxy-using-alb/
ブログのインデックス作成
辞書 BLOG
にインデックスを作成します。
それぞれに URL
情報を入れていきます。
BLOG = {} for i, url in enumerate(url_list): BLOG[i] = {} BLOG[i]["url"] = url print("len(BLOG): {}".format(len(BLOG))) print("BLOG[0][\"url\"]: {}".format(BLOG[0]["url"])) # len(BLOG): 48 # BLOG[0]["url"]: https://dev.classmethod.jp/cloud/aws/on-premise-reverse-proxy-using-alb/
URLからブログのテキストを取得
各URLから ./util/get_blog_texts.py を使ってブログテキスト(strのlist
)を取得します。
from util.get_blog_texts import get_blog_texts import time for i in BLOG.keys(): url = BLOG[i]["url"] print("#{} getting texts from: {}".format(i, url)) BLOG[i]["texts"] = get_blog_texts(url) time.sleep(1)
▼確認
print("len(BLOG[0][texts]): {}".format(len(BLOG[0]["texts"]))) print("len(BLOG[0][texts][0]): {}".format(BLOG[0]["texts"][0])) # len(BLOG[0][texts]): 65 # len(BLOG[0][texts][0]): Application Load Balancer (ALB), Network Load Balancer (NLB) はターゲットにローカルIPアドレス を指定できます。VPCピアリング接続先のインスタンスや、Direct Connect・VPN接続先のオンプレのサーバーをターゲットグループに登録することができます。
テキストの前処理
今回は日本語テキストを想定しているので、空白を削除します。 また、アルファベットは全て小文字化します。
※ この部分は後述する Janome(Pythonの日本語形態素解析パッケージ) の前処理機能でも対応はできます。
for i in BLOG.keys(): BLOG[i]["texts"] = [t.replace(' ','').lower() for t in BLOG[i]["texts"]] print("len(BLOG[0][texts]): {}".format(len(BLOG[0]["texts"]))) print("len(BLOG[0][texts][0]): {}".format(BLOG[0]["texts"][0])) # len(BLOG[0][texts]): 65 # len(BLOG[0][texts][0]): applicationloadbalancer(alb),networkloadbalancer(nlb)はターゲットにローカルipアドレスを指定できます。vpcピアリング接続先のインスタンスや、directconnect・vpn接続先のオンプレのサーバーをターゲットグループに登録することができます。
やってみた #Janome で形態素解析編
前章で各ブログのテキストデータを取得しました。
これらテキストの形態素解析を Janome を使って行います。
Janome (蛇の目; ◉) は,Pure Python で書かれた,辞書内包の形態素解析器です。
依存ライブラリなしで簡単にインストールでき, アプリケーションに組み込みやすいシンプルな API を備える形態素解析ライブラリを目指しています。
インポート〜字句解析器の作成
以下インポートして、 Analyzer
を作成します。
- Tokenizer: 字句解析器
- POSStopFilter: 記号や助詞を取り除くために使用します
- Analyzer: 前処理、後処理を含めた字句解析のフレームワーク
from janome.tokenizer import Tokenizer from janome.analyzer import Analyzer from janome.tokenfilter import POSStopFilter tokenizer = Tokenizer() token_filters = [POSStopFilter(['記号','助詞','助動詞','動詞'])] a = Analyzer(tokenizer=tokenizer, token_filters=token_filters)
Analyzer
のテストしてみましょう。
ブログの1文を解析してみます。
test_tokens = a.analyze(BLOG[0]["texts"][0]) for t in test_tokens: print(t)
(カッコも名詞になっていますが) 一覧を取得できてそうですね。
各ブログの名詞を分かち書きして登録
スペース区切りで単語を書いていくことを 分かち書き といいます。
- 各ブログのテキストの名詞を抽出して、
- 結果を分かち書きして
BLOG[i]["wakati"]
へ登録します。
# 解析 for i in BLOG.keys(): texts_flat = "。".join(BLOG[i]["texts"]) tokens = a.analyze(texts_flat) BLOG[i]["wakati"] = ' '.join([t.surface for t in tokens]) # 確認 print("BLOG[0][wakati]: {}".format(BLOG[0]["wakati"]))
1ブログの 分かち書き結果は以下のようになりました。
やってみた #scikit-learn で単語の出現頻度計算編
Pythonの機械学習ライブラリで有名な scikit-learn の CountVectorizer を使って単語の出現頻度を計算します。
それぞれのブログの各単語の出現頻度を Bag of Words:BoW (単語の袋) として格納していきます。
Bag of Words: BoW とは
簡単に BoW を説明します。 BoW はある文書の単語の出現回数をベクトルで表したものとなります。
以下 2文書があったとします。
- 文書1: 私はEmacsが好きです
- 文書2: 私はVimが嫌いです
この文書1, 文書2の BoW はそれぞれ以下の行で表されるベクトルです。
私 | は | Emacs | Vim | が | 好き | 嫌い | です | |
---|---|---|---|---|---|---|---|---|
文書1 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 1 |
文書2 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 1 |
列の各単語がその文書内にいくつ出現するか を表すのが BoW です。
後述の TF-IDF 計算で必要となってきます。
各ブログの BoW を計算する
CountVectorizer
を作成します。
from sklearn.feature_extraction.text import CountVectorizer import random vectorizer = CountVectorizer()
vectorizer.fit_transform を使って全ブログの BoW を計算します。
結果(各ブログの BoW ベクトル) を BLOG[i]["bow"]
に格納します。
X = vectorizer.fit_transform([BLOG[i]["wakati"] for i in BLOG.keys()]) for i, bow in enumerate(X.toarray()): BLOG[i]["bow"] = bow
また、vectorizer.get_feature_names で計算したBoW のインデックスに対応する単語の情報を
配列として取得できます (WORDS
とします)。
WORDS = vectorizer.get_feature_names() print(WORDS)
試しに 1ブログで高頻出 ( 6
回以上出現) だった単語をリストしてみましょう。
一番多い単語は ALB
であることが分かりました。ブログの内容(↓)的に妥当そうですね。
やってみた #TF-IDFを計算してみよう編
(ようやく) メインの内容です。
- TF-IDF の導出関数を作成します
- ブログに出てくる単語の TF-IDF値をみてみます
今回は勉強のため手動で TF-IDF 導出関数を実装しています。 実際は scikit-learnのライブラリに TfidfVectorizer というものがあり、簡単に TF-IDFを求めることができます。
TF(単語の出現頻度) を計算する
TFの定義を再掲します。
x1
: 文書d における全単語の出現回数の和y1
: 文書d における単語wの出現回数x2
: 単語w を含む文書数y2
: 全文書数
以下 calc_tf
関数を作成しました。
def calc_tf(b_idx, w_idx): """b_idx 番目のブログの WORD[w_idx] の TF値を算出する""" # WORD[w_idx] の出現回数の和 word_count = BLOG[b_idx]["bow"][w_idx] if word_count == 0: return 0.0 # 全単語の出現回数の和 sum_of_words = sum(BLOG[b_idx]["bow"]) # TF値計計算 return word_count/float(sum_of_words)
試しに 1ブログの TF値を降順で表示してみました。
index = 0 print("# TF values of blog:{}".format(BLOG[index]["url"])) sample_tfs = [calc_tf(index, w_idx) for w_idx, word in enumerate(WORDS)] tfs_sorted = sorted(enumerate(sample_tfs), key=lambda x:x[1], reverse=True) for i, tf in tfs_sorted[:20]: print("{}\t{}".format(WORDS[i], round(tf, 4)))
alb, オンプレサーバー
といったブログの特徴を表しそうな単語がある一方で、
作成, こと
といったあまり重要じゃない単語も確認できますね。
IDF(単語のレア度) を計算する
IDFの定義を再掲します。
x1
: 文書d における全単語の出現回数の和y1
: 文書d における単語wの出現回数x2
: 単語w を含む文書数y2
: 全文書数
以下 calc_idf
関数を作成しました。
(分母はゼロにならないように +1
しています。)
import math def calc_idf(w_idx): """WORD[w_idx] の IDF値を算出する""" # 総文書数 N = len(BLOG.keys()) # 単語 word が出現する文書数 df を計算 df = len([i for i in BLOG.keys() if BLOG[i]["bow"][w_idx] > 0]) # idf を計算 return math.log2(N/float(df + 1))
全単語のIDF値を計算して IDFS
へ格納します。
IDFS = [calc_idf(w_idx) for w_idx, word in enumerate(WORDS)]
IDFの値を降順に並べた結果(上位・下位 10個)を出してみます。
idfs_sorted = sorted(enumerate(IDFS), key=lambda x:x[1], reverse=True) print("# IDF values") for w_idx, idf in idfs_sorted[:10]: print("{}\t{}".format(WORDS[w_idx], round(idf, 4))) print("︙") for w_idx, idf in idfs_sorted[-10:]: print("{}\t{}".format(WORDS[w_idx], round(idf, 4)))
IDF値の低いものに 以下, こと, ため
といった日本語でよく使われる言い回しが出てきましたね。
IDF値の高いものは、ぱっと見て数字列が列挙されましたが、 IDF値の一番高い単語 は他にもたくさんあります(↓)。
これらは対象のブログ本数が48本のうち 1本のみで使われていた単語です。
TF-IDF(単語の重要度)を計算する
TF-IDFの定義を再掲します。
x1
: 文書d における全単語の出現回数の和y1
: 文書d における単語wの出現回数x2
: 単語w を含む文書数y2
: 全文書数
先程実装した calc_tf, calc_idf
を使います。
def calc_tfidf(b_idx, w_idx): """b_idx 番目のブログの WORD[w_idx] の TF-IDF値を算出する""" return calc_tf(b_idx, w_idx) * calc_idf(w_idx)
TFのときと 同じように 1ブログの TF-IDF値を降順で表示してみました。
ブログ(オンプレサーバーをターゲットにしたALBリバースプロキシ環境を構築してみる) の TF, TF-IDFの上位値は以下のようになりました。
TF上位 | TF-IDF上位 | |
---|---|---|
1 | alb | alb |
2 | 作成 | オンプレサーバー |
3 | ターゲット | ターゲット |
4 | 構築 | サーバー |
5 | 環境 | http |
6 | オンプレサーバー | app |
7 | ip | 構築 |
8 | サーバー | 外部 |
9 | サービス | 検証 |
10 | 指定 | ip |
11 | 接続 | エイリアス |
12 | 検証 | リバースプロキシ |
13 | app | ローカル |
14 | http | 接続 |
15 | vpc | yaml |
16 | こと | 通信 |
17 | ローカル | 80 |
18 | 通信 | 登録 |
19 | vpn | awscloudformation |
20 | yaml | vpn |
TF上位だった 作成, こと
などが消えて、
TF上位に無かった リバースプロキシ, awscloudformation
などが TF-IDF上位に上がっていることが分かります。
色んなブログのTF-IDFを見てみる
長い長い前処理があって、ようやく全ブログの全名詞の TF-IDF値を取得できました。
色んなブログの TF-IDF値を見てみましょう。
TF上位 | TF-IDF上位 | |
---|---|---|
1 | コンテナ | コンテナ |
2 | ec | fargate |
3 | 起動 | 起動 |
4 | こと | ecs |
5 | 利用 | タイプ |
6 | fargate | イメージ |
7 | サービス | docker |
8 | aws | ec |
9 | 環境 | スケール |
10 | タイプ | タスク |
fargate
や docker
といったワードが高い TF-IDF値となっていますね。
TF上位 | TF-IDF上位 | |
---|---|---|
1 | aws | active |
2 | vpn | 自宅 |
3 | 自宅 | vpn |
4 | 接続 | cisco |
5 | active | 接続 |
6 | cisco | フェーズ |
7 | 確認 | ike |
8 | 作成 | globalip |
9 | フェーズ | idle |
10 | ルータ | qm |
AWS系のブログが多いため、単語 AWS
は TF-IDF値は高くありません。
その代わり Ciscoルータでサイト間VPNする際によく出る用語 ike, qm, idle
あたりが TF-IDF上位に上がっています。
TF上位 | TF-IDF上位 | |
---|---|---|
1 | キャスト | キャスト |
2 | マルチ | マルチ |
3 | tgw | 受信 |
4 | transitgateway | eni |
5 | ため | tgw |
6 | グループ | transitgateway |
7 | 作成 | マルチキャストグループ |
8 | 受信 | マルチキャストルータ |
9 | eni | レシーバー |
10 | ip | 待ち |
マルチ/キャストと分解されていますが、 TGWマルチキャストで必要な構成要素が一部 TF-IDF上位に上がっています。
▼ CloudTrail, Athena, S3関連のブログ
TF上位 | TF-IDF上位 | |
---|---|---|
1 | バケット | バケット |
2 | パブリック | cloudtrail |
3 | 作成 | 投稿 |
4 | cloudtrail | パブリック |
5 | オブジェクト | オブジェクト |
6 | 投稿 | 証跡 |
7 | ログ | athena |
8 | リクエスト | putobject |
9 | athena | リクエスト |
10 | 格納 | ログ |
TF上位一覧もそれなりにブログの特徴を捉えていますが、
TF-IDFのほうが 証跡, putobject
といった単語があります。より良いですね。
TF上位 | TF-IDF上位 | |
---|---|---|
1 | emacs | emacs |
2 | 設定 | mode |
3 | mode | ido |
4 | インストール | org |
5 | org | homebrew |
6 | 環境 | インストール |
7 | 表示 | xcode |
8 | 記事 | ターミナル |
9 | homebrew | mac |
10 | ido | フォント |
入社すぐに書いたEmacs Org-mode布教用のブログです。
セットアップで入れたもの ido-mode, homebrew, xcode
あたりが TF-IDF上位に入りました。
おわりに
自分のブログの各単語の TF-IDF値を調べてみました。 今回はTF-IDFの理解のために色々と手を動かしました。
今回の Tryで使用した Notebookは以下にあります。
AWSのサービスに Amazon Comprehend があるので、 これらを使うことで更に少ないコード量( or ノンコーディング) でテキストの分析ができそうです。
- アップデート Amazon Comprehendで日本語テキストの分析ができるようになりました | Developers.IO
- 私の文章が「暗い」かどうかはAmazon Comprehendにハッキリしてもらう Analysis jobsで20000字を感情分析してみた | Developers IO
SageMakerとの連携もできるみたいなので、今後はそちら触っていこうと思います。